import { getInitOptions } from '@/config/init'
import axios from 'axios'
import fs from 'fs'

let uid = 100000

export function genUid (): string {
  const now = String(Date.now())
  const r = String(Math.random()).slice(-6)
  return now + r + String(uid++)
}

export interface RawaOssOptions {
  accessKeyId: string;

  accessKeySecret: string;

  stsToken?: string;

  bucket?: string;

  endpoint?: string;

  region?: string;

  secure?: boolean;
}

export interface RawaOssMultipartUploadOptions {
  parallel?: number;
  /** the suggested size for each part */
  partSize?: number;
  /** the progress callback called after each successful upload of one part */
  progress?: (...args: any[]) => any;
  /** the checkpoint to resume upload, if this is provided, it will continue the upload from where interrupted, otherwise a new multipart upload will be created. */
  checkpoint?: RawaOssCheckpoint;
  timeout?: number;
}

export interface RawaOssMultipartUploadResult {
  bucket: string;
  name: string;
  data: any;
  res: RawaOssNormalSuccessResponse;
  etag: string;
}

export interface RawaOssCheckpoint {
  /** The file object selected by the user, if the browser is restarted, it needs the user to manually trigger the settings */
  file: File;
  /** object key */
  name: string;
  fileSize: number;
  partSize: number;
  uploadId: string;
  doneParts: Array<{ number: number; etag: string }>;
}

export interface RawaOssNormalSuccessResponse {
  /** response status */
  status: number;
  /** response headers */
  /** todo the object in detail */
  headers: any;
  /** response size */
  size: number;
  /**  request total use time (ms) */
  rt: number;

  requestUrls?: string[];
}

interface RawaOssSignatureUrlOptions {
  /** after expires seconds, the url will become invalid, default is 1800 */
  expires?: number;
  /** the HTTP method, default is 'GET' */
  // method?: HTTPMethods;
  /** set the request content type */
  'Content-Type'?: string;
  process?: string;
  /** set the response headers for download */
  response?: RawaOssResponseHeaderType;
}

interface RawaOssResponseHeaderType {
  'content-type'?: string;
  'content-disposition'?: string;
  'cache-control'?: string;
}

interface RawaOssRequestOptions {
  // the operation timeout
  timeout?: number;
}

export interface RawaOssChunkData {
  number: number;
  blob: Blob;
  total: number;
}

export class NodeFile {
  path = ''

  size = 0

  constructor (path: string, size: number) {
    this.path = path
    this.size = size
  }

  getChunk (n: number, chunkSize: number, size: number): Blob {
    // return new Promise((resolve, reject) => {
    //   const ret: any[] = []
    //   const start = n * chunkSize
    //   const rs = fs.createReadStream(this.path, { start, end: Math.min(chunkSize + start, this.size) })
    //   rs.on('data', (data) => {
    //     ret.push(data)
    //   })
    //   rs.on('end', () => {
    //     resolve(new Blob(ret))
    //   })
    //   rs.on('error', (err) => {
    //     reject(err)
    //   })
    // })
    const start = n * chunkSize
    const len = Math.min(size - start, chunkSize)
    const buffer = Buffer.alloc(len)
    let fd
    try {
      fd = fs.openSync(this.path, 'r')
      fs.readSync(fd, buffer, 0, len, start)
    } finally {
      if (fd !== undefined) fs.closeSync(fd)
    }
    return new Blob([buffer])
  }
}

const camelizeRE = /-(\w)/g
const camelize = (str: string) => {
  return str.toLowerCase().replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}

interface HttpUploadQueueItem {
  key: string;

  uploading: boolean;

  fn: () => Promise<any>;
}

const httpUploadQueue: HttpUploadQueueItem[] = []

export default class RawaOss {
  constructor (options: RawaOssOptions) {
    // this.options = {
    //   accessKeyId: '',
    //   accessKeySecret: '',
    //   stsToken: '',
    //   bucket: 'fs',
    //   endpoint: 'localhost:50400',
    //   region: '',
    //   secure: false
    // }
    this.options = options
  }

  options!: RawaOssOptions

  checkpoint!: RawaOssCheckpoint

  queue: Array<{number: number; source: any}> = []

  uploadOptions!: RawaOssMultipartUploadOptions

  resolve: any = null

  reject: any = null

  aborted = false

  paused = false

  multipartUpload (name: string, file: any, options: RawaOssMultipartUploadOptions = {}): Promise<RawaOssMultipartUploadResult> {
    const {
      parallel,
      partSize,
      progress,
      checkpoint,
      timeout
    } = options
    this.uploadOptions = options
    this.checkpoint = checkpoint || this.initCheckPoint(name, file, partSize)

    return new Promise((resolve, reject) => {
      this.resolve = resolve
      this.reject = reject
      let resolved = false
      const handler = () => {
        const chunkData = this.getChunk()
        const completeFn = () => {
          resolve({
            bucket: this.options.bucket as string,
            name: name,
            data: null as any,
            etag: '',
            res: {
              status: 200,
              headers: {},
              size: this.checkpoint.fileSize,
              rt: 0,
              requestUrls: [`${this.getBaseUrl()}${this.getRelativePath()}`]
            }
          })
        }
        if (!chunkData && this.queue.length < 1) { // 已上传完成
          if (!resolved) {
            completeFn()
            resolved = true
          }
          return
        }
        if (chunkData && !this.paused && !this.aborted) {
          const source = axios.CancelToken.source()
          this.queue.push({ number: chunkData.number, source })
          this.upload(chunkData, source).then(() => {
            const idx = this.queue.findIndex((v: any) => v.number === chunkData.number)
            if (idx >= 0) this.queue.splice(idx, 1)
            this.checkpoint.doneParts.push({ number: chunkData.number, etag: '' })

            if (progress) {
              const p = this.checkpoint.doneParts.length / chunkData.total
              progress(p, this.checkpoint)
            }

            const cd = this.getChunk()
            if (!cd && this.queue.length < 1) {
              completeFn()
            } else {
              handler()
            }
          }).catch(e => {
            reject(e)
          })
        }
      }

      [...Array(parallel || 5).keys()].forEach(v => {
        handler()
      })

      // {
      //   bucket: this.options.bucket as string,
      //   name: name,
      //   data: null as any,
      //   res: {
      //     status: 200,
      //     headers: {},
      //     size: this.checkpoint.fileSize,
      //     rt: 0,
      //     requestUrls: []
      //   }
      // }
    })
  }

  signatureUrl (name: string, options?: RawaOssSignatureUrlOptions): string {
    // const bu = this.getBaseUrl()
    // const pathname = new URL(bu).pathname
    // const h = bu.replace(pathname, '')
    // const u = new URL(h + '/' + name)

    const b = getInitOptions()?.api + '/api/oss/download'
    const f = '/download/'
    const p = name.substr(name.indexOf(f) + f.length - 1)
    const u = new URL(b + p)
    const response = options?.response || {}
    Object.entries(response).forEach(([k, v]) => {
      u.searchParams.append(camelize(k), v)
    })
    return u.href
  }

  cancel () {
    this.paused = true
    this.queue.forEach(v => v.source.cancel())
    if (this.reject) this.reject()
  }

  abortMultipartUpload (name: string, uploadId: string, options?: RawaOssRequestOptions): Promise<RawaOssNormalSuccessResponse> {
    this.aborted = true
    this.queue.forEach(v => v.source.cancel())
    if (this.reject) this.reject({ name: 'abort' })
    return Promise.resolve(null as any)
  }

  isCancel () {
    return this.paused
  }

  private initCheckPoint (name: string, file: File, partSize = 1024 * 1024): RawaOssCheckpoint {
    const maxParts = 10000
    const size = file.size
    partSize = Math.max(Math.ceil(size / maxParts), partSize)

    return {
      file,
      name,
      fileSize: size,
      partSize,
      uploadId: genUid(),
      doneParts: []
    }
  }

  private getChunk (): RawaOssChunkData | null {
    const { fileSize, partSize } = this.checkpoint
    const set = new Set([...this.checkpoint.doneParts.map(v => v.number), ...this.queue.map(v => v.number)])
    const total = Math.ceil(fileSize / partSize)
    if (set.size >= total) return null
    let number = 0
    for (let i = 0; i < total; i++) {
      if (!set.has(i)) {
        number = i
        break
      }
    }
    const start = number * partSize
    const end = Math.min(start + partSize, fileSize)
    if (this.checkpoint.file instanceof File) {
      const blob = this.checkpoint.file.slice(start, end)
      return { number, blob, total }
    } else {
      const blob = (this.checkpoint.file as NodeFile).getChunk(number, partSize, fileSize)
      return { number, blob, total }
    }
  }

  private getRequest (type: 'upload' | 'download' = 'download') {
    return axios.create({
      baseURL: this.getBaseUrl(type),
      timeout: this.uploadOptions.timeout || 30 * 60 * 1000,
      withCredentials: true
    })
  }

  private upload ({ number, blob, total }: RawaOssChunkData, source: any): Promise<any> {
    const max = 5
    const handle = () => {
      const num = httpUploadQueue.filter(v => v.uploading).length
      const todos = httpUploadQueue.filter(v => !v.uploading)
      if (num >= 5) return
      const pendings = todos.slice(0, max - num)
      pendings.forEach(v => {
        v.uploading = true
        v.fn().finally(() => {
          const idx = httpUploadQueue.findIndex(w => w.key === v.key)
          if (idx >= 0) {
            httpUploadQueue.splice(idx, 1)
          }
          handle()
        })
      })
    }
    return new Promise((resolve, reject) => {
      const action = () => {
        const reuqest = this.getRequest('upload')
        const data = new FormData()
        data.append('file', blob)
        return reuqest.post(this.getRelativePath(),
          data,
          {
            cancelToken: source.token,
            params: {
              chunkNum: number,
              chunkTotal: total,
              size: this.checkpoint?.fileSize,
              uploadId: this.checkpoint.uploadId
            }
          }
        ).then(res => {
          resolve(res)
        }).catch((err) => {
          reject(err)
        })
      }
      const item: HttpUploadQueueItem = {
        key: genUid(),
        uploading: false,
        fn: action
      }
      httpUploadQueue.push(item)
      handle()
    })
  }

  private getBaseUrl (type: 'upload' | 'download' = 'download') {
    return getInitOptions()?.api + '/api/oss/' + type + '/' + this.options.bucket
  }

  private getRelativePath (name?: string) {
    return `/${name || this.checkpoint?.name}`
  }
}
